package cn.geminis.auth.config;

import cn.geminis.auth.vo.LoginResult;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
import org.springframework.security.authentication.event.AuthenticationFailureServiceExceptionEvent;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.savedrequest.CookieRequestCache;
import org.springframework.security.web.savedrequest.DefaultSavedRequest;
import org.springframework.util.StringUtils;

import java.io.File;
import java.io.IOException;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;

/**
 * @author puddi
 */
@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
@Slf4j
@RequiredArgsConstructor
public class AuthorizationServerConfig {

    @Value("${geminis.auth.loginPage}")
    private String loginPage;

    @Value("${geminis.auth.redirectPrefix}")
    private String redirectPrefix;

    @Value("${geminis.auth.pfx.file:}")
    private String pfxFile;

    @Value("${geminis.auth.pfx.password:}")
    private String pfxPassword;

    private final DefaultAuthenticationEventPublisher defaultAuthenticationEventPublisher;

    private static void handleResult(HttpServletResponse resp, LoginResult loginResult) throws IOException {
        resp.setContentType("application/json;charset=utf-8");
        var out = resp.getWriter();
        out.write(new ObjectMapper().writeValueAsString(loginResult));
        out.flush();
        out.close();
    }

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(config -> {
            config.requestMatchers("/password")
                    .permitAll();
        });

        defaultAuthenticationEventPublisher.setDefaultAuthenticationFailureEvent(AuthenticationFailureServiceExceptionEvent.class);

        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                .oidc(Customizer.withDefaults());

        var requestCache = new CookieRequestCache();
        http.requestCache(config -> config.requestCache(requestCache));
        http.securityMatcher("/**")
                .formLogin(config -> config
                        .loginPage(loginPage)
                        .loginProcessingUrl("/login")
                        .successHandler((req, resp, authentication) -> {
                            if (log.isInfoEnabled()) {
                                log.info("用户认证通过，用户：{}", authentication.getName());
                            }
                            var savedRequest = (DefaultSavedRequest) requestCache.getRequest(req, resp);
                            var returnUrl = this.redirectPrefix + ((savedRequest != null) ? (savedRequest.getRequestURI() + "?" + savedRequest.getQueryString()) : "");
                            requestCache.removeRequest(req, resp);
                            var loginResult = LoginResult.builder()
                                    .status(302)
                                    .location(returnUrl)
                                    .build();
                            handleResult(resp, loginResult);
                        })
                        .failureHandler((req, resp, ex) -> {
                            if (log.isInfoEnabled()) {
                                log.info("用户认证失败，{}", ex.toString());
                            }
                            var loginResult = LoginResult.builder()
                                    .status(401)
                                    .msg(ex.getMessage())
                                    .build();
                            handleResult(resp, loginResult);
                        }));
        http.csrf(AbstractHttpConfigurer::disable);

        return http.build();
    }

    @Bean
    public JWKSource<SecurityContext> jwkSource() throws CertificateException, KeyStoreException, IOException, NoSuchAlgorithmException, JOSEException {
        RSAPublicKey publicKey;
        RSAPrivateKey privateKey;
        if (StringUtils.hasText(pfxFile)) {
            var pfx = KeyStore.getInstance(new File(pfxFile), pfxPassword.toCharArray());
            var alias = pfx.aliases().nextElement();
            var importKey = RSAKey.load(pfx, alias, pfxPassword.toCharArray());
            publicKey = importKey.toRSAPublicKey();
            privateKey = importKey.toRSAPrivateKey();

        } else {
            var keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            var keyPair = keyPairGenerator.generateKeyPair();
            publicKey = (RSAPublicKey) keyPair.getPublic();
            privateKey = (RSAPrivateKey) keyPair.getPrivate();
        }
        var key = new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();

        var jwkSet = new JWKSet(key);
        return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
    }

    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8();
    }

}
